package com.baoxin.service;

import com.baoxin.dao.UserDao;
import com.baoxin.domain.QDepartment;
import com.baoxin.domain.QUser;
import com.baoxin.domain.User;
import com.baoxin.dto.UserDTO;
import com.baoxin.dto.UserDeptDTO;
import com.querydsl.core.QueryResults;
import com.querydsl.core.types.ExpressionUtils;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.Query;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @Author: Rose
 * @Date: 2021/3/23 17:32
 * @version: 1.0.0
 */

@Service
@Transactional
public class UserService {

    @Autowired
    private JPAQueryFactory jpaQueryFactory;

    @Autowired
    private UserDao userDao;


    /**
     * 以下 展示 使用原生的DSL
     * <p>
     * <p>
     * <p>
     * 根据用户名和密码查找（假定 只能找出一条）
     *
     *   第一个方法是根据用户名和密码进行查询, QUser 中有一个静态user 属性, 直接生成QUser的实例,QuserDSL 都是围绕这个QXxxx 来进行操作的
     *   代码很直观, selectFrom 是select 和 from 方法的合并, 这里为了方便就不分开写了. where 中可以接收可变参数Predicate , 由于Predicate
     *   是一个接口, 有user.username.eq或者user.uIndex.gt 等等方法返回的都是BooleanExpression 或者XXExpression,这些XXXExpression 都是Predicate
     *   的实现,故直接传入,让QueryDSL 在内部做处理, 其实Predicate 也是实现了Expression 接口, 大家如果有兴趣可以自行研究跟踪源码
     *
     * @param username
     * @param password
     * @return
     */
    public User findByUserNameAndPassword(String username, String password) {
        QUser user = QUser.user;
        return jpaQueryFactory.selectFrom(user).where(
                user.userName.eq(username),
                user.password.eq(password)
        ).fetchOne();
    }


    /**
     * 查询所有的实体， 根据用户名 字段排序
     *
     *  第二个方法相当直观, 更sql 的字面意思几乎一模一样.
     *
     *
     * @return
     */
    public List<User> findAll() {
        QUser user = QUser.user;

        return jpaQueryFactory.selectFrom(user)
                .orderBy(
                        user.userName.asc()
                ).fetch();
    }


    /**
     * 分页查询所有的实体， 根据useranem 排序
     *
     *  第三个方法是第二个方法的排序写法, 主要用到了 offset,limit 方法,根据传入的pageable 参数进行分页, 最后返回的结果是一个QuerResults 类型
     *  的返回值, 该返回值对象简单的封装了一些 分页的参数与返回的实体集, 然调用者自己根据需求去取出使用.
     *
     * @param pageable
     * @return
     */

    public QueryResults<User> findAllPage(Pageable pageable) {
        QUser user = QUser.user;

        QueryResults<User> userQueryResults = jpaQueryFactory.selectFrom(user)
                .orderBy(
                        user.userName.asc()
                ).offset(pageable.getOffset()) // 起始页
                .limit(pageable.getPageSize()) // 每页大小
                .fetchResults();// 获取结果， 该结果封装了实体集合，分页的信息，需要这些信息直接从该实体对象里面拿去即可
        return userQueryResults;
    }


    /**
     *  根据起始日期与终止日期 查询
     *
     *  第四个方法展示了日期查询,也相当直观, 大家尝试了就知道了, 主要使用到了 between 方法
     */
    public List<User> findByBirthdayBetween(Date start , Date end ) {
        QUser user = QUser.user;
        return jpaQueryFactory.selectFrom(user)
                .where(
                        user.birthday.between(start,end)
                ).fetch();
    }


    /**
     *
     *  部分字段映射查询，
     *   投影为UserRes, lambda方式（灵活， 类型可以在lambda 中修改）
     *
     *   第五个方法 是比较重要的方法, 这个展示了 如何进行部分字段的映射查询, 这个方法的目的是只查询username, id , name, birthday四个字段
     *   然后封装到UserDTO中, 最后返回. 其中,由于select 与from 拆分了以后返回的泛型类型就是tuple 类型(Tuple是一个接口,他可以根据tuple.get(QUser.username)
     *   的方式获取User.username 的真实值, 暂时将他理解为 一个类型安全的Map就行) ,根据pageable 参数做了分页处理, fetch 之后就返回一个list<Tuple>对象. 从fetch
     *   方法之后,使用到了Stream , 紧接着使用Java 8 的高阶函数map, 这个map 函数的作用是将List<Tuple> 中的tuple 元素依次转换成String 类型. 最后
     *   通过collect结束stream, 返回一个我们需要的List<UserDTO>.
     *
     * @param pageable
     * @return
     */
    public List<UserDTO> findAllUserDto(Pageable pageable) {
        QUser user = QUser.user;
        List<UserDTO> dtoList = jpaQueryFactory
                .select(
                        user.userName,
                        user.id,
                        user.name,
                        user.age,
                        user.sex
                ).from(user)
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch()
                .stream()
                .map(tuple -> UserDTO.builder()
                    .userName(tuple.get(user.userName))
                    .id(tuple.get(user.id))
                    .age(tuple.get(user.age))
                    .sex(tuple.get(user.sex))
                    .name(tuple.get(user.name))
                    .birthday(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(tuple.get(user.birthday)))
                    .build()
                ).collect(Collectors.toList());

        return dtoList;
    }

    /**
     *  部分字段映射查询
     *
     *      投影为 UserRes, 自带的Projections 方式， 不够灵活， 不能转换 类型， 但是可以使用as 转换名字
     *
     *  第六个方法与第五个方法的效果相同, 使用QueryDSL 的Projections 实现. 但是有一点, 当User实体的属性类型与UserDTO中的属性类型不相同时,
     *  不方便转换. 除了属性类型相同时转换方便以外, 还是建议使用map 函数进行操作.
     * @return
     */
    public List<UserDTO> findAllDTO(){
        QUser user = QUser.user;
        List<UserDTO> dtoList = jpaQueryFactory
                .select(
                        Projections.bean(
                                UserDTO.class,
                                user.name,
                                user.userName,
                                user.sex ,
                                user.id,
                                user.sex
                        )
                ).from(user)
                .fetch();

        return dtoList;
    }


    /**
     *  删除用户
     *
     *
     * @param username
     * @return
     */
    public long deleteUser(String username) {
        QUser user = QUser.user;
        return jpaQueryFactory.delete(user).where(user.userName.eq(username)).execute();
    }


    /**
     *  更新记录
     *
     * @param user
     * @param username
     * @return
     */
    public  long updateUser(final  User user ,final String username) {
        QUser quser = QUser.user;
        BooleanExpression usernameExpression = quser.userName.eq(username);
        return jpaQueryFactory.update(quser).where(usernameExpression)
                .set(quser.name,user.getName())
                .set(quser.password,user.getPassword())
                .set(quser.age,user.getAge())
                .execute();
    }

    /**
     *
     *  使用原生的 Query
     *
     * @param username
     * @return
     */
    public User findOneUserByOriginalSql(String username) {
        QUser user = QUser.user;
        Query query = jpaQueryFactory.selectFrom(user)
                .where(user.userName.eq(username)).createQuery();
        return (User) query.getSingleResult();
    }

    /**
     *
     *  分页查询列表
     *
     * @param offset
     * @param pageSize
     * @return
     */
    public List<User> findAllUsers(int offset, int pageSize ) {
        QUser user = QUser.user;

        BooleanExpression idExpression = user.id.lt(10);

        Sort id = new Sort(new Sort.Order(Sort.Direction.DESC, "id"));

        PageRequest pageRequest = new PageRequest(offset, pageSize, id);

        Page<User> all = userDao.findAll(idExpression, pageRequest);

        return all.getContent();
    }



    /**
     *
     *
     *   以下 展示与 SpringDataJpa 整合的 dsl
     *
     *
     *
     * @return
     */

    /**
     *
     *  根据 姓名和用户名 查询， 并且根据用户名 排序
     *
     *
     *
     *   第7和第8个方法 展示了QueryDSL 与SpringDataJPA 的联合shiyong,由于我们的UserDao 继承了QueryPredicateExecutor, 所以获取
     *   了联合使用的支持,
     *
     *  QueryDslPredicateExcutor 里面的方法大多数可以传入Predicate 类型的参数, 说明还是可以围绕着QUser 来进行操作的
     *
     * @param name
     * @param userName
     * @return
     */
    public List<User> findByNameAndUserName(String name ,String userName ) {
        QUser user = QUser.user;
        List<User> users = (List<User>) userDao.findAll(
                user.name.eq(name)
                .and(user.userName.eq(userName)),
                user.userName.asc()  // 排序参数
        );

        return users;
    }

    /**
     *
     *  统计名字 像 username 的记录数量
     *
     * @param username
     * @return
     */
    public long countByUserNameLike(String username) {
        QUser user = QUser.user;
        return userDao.count(user.userName.like("%"+ username+"%"));
    }


    /**
     *
     *  展示动态 dsl 查询
     *
     * @return
     */


    /**
     *
     *   所有条件动态分页查询
     *
     * @param pageable
     * @param username
     * @param password
     * @param name
     * @param age
     * @return
     */
    public Page<User> findByUserProperties(Pageable pageable, String username ,String password, String name ,Integer age ) {
        QUser user = QUser.user;
        // 初始化组装条件（类似where 1=1）
        Predicate predicate = user.isNotNull().or(user.isNull());

        // 执行动态条件拼接
        predicate = username == null ? predicate: ExpressionUtils.and(predicate,user.userName.eq(username));
        predicate = password == null ? predicate: ExpressionUtils.and(predicate,user.password.eq(password));
        predicate = name == null ? predicate: ExpressionUtils.and(predicate,user.name.eq(name));
        predicate = age  == null ? predicate: ExpressionUtils.and(predicate,user.age.eq(age));

        Page<User> userList = userDao.findAll(predicate,pageable);

        return userList;

    }


    /**
     *
     *  动态条件查询, 分组查询
     *
     * @param username
     * @param password
     * @param name
     * @param sex
     * @return
     */
    public List<User> findByUserPropertiesGroupByUsername(String username ,String password, String name ,Integer sex ) {
        QUser user = QUser.user;
        // 初始化 组装条件 (类似 where 1=1 )
        Predicate predicate = user.isNotNull().or(user.isNull()) ;
        // 执行动态条件封装.
        predicate = username ==null ? predicate : ExpressionUtils.and(predicate,user.userName.eq(username));
        predicate = name ==null ? predicate : ExpressionUtils.and(predicate,user.name.eq(name));
        predicate = password ==null ? predicate : ExpressionUtils.and(predicate,user.password.eq(password));
        predicate = sex ==null ? predicate : ExpressionUtils.and(predicate,user.sex.eq(sex));

        // 执行拼装好的条件并根据 id 排序, 根据id进行分组
        List<User> list = jpaQueryFactory
                .selectFrom(user)
                .where(predicate)
                .orderBy(user.id.asc())
                .groupBy(user.id)
                .having(user.id.longValue().max().gt(7)) // id 最大值小于7
                .fetch();
        return list;
    }


    /**
     *
     *
     *  最后,我们的杀手锏来了, JPA对动态条件拼接查询支持一向都不太灵活,如果在mybatis 中, 我们使用if标签就可以很容易实现动态查询
     *
     *  但是JPA 中, 就不是那么方便了. SpringDataJPA 给我们提供了 Specification + JpaSpecificationExecutor 帮我们解决, 但是,
     *
     *  在需要编写某些复杂的动态条件拼接,分组之后又进行动态拼接的复杂查询, 可能就显得力不从心了. 这个时候可能需要直接对entityManager
     *
     *  操作, 然后对SQL进行拼接.
     *
     *
     *  我们userservice 中的最后两个方法就是展示如何做动态查询的, 第一个方法结合而来与SpringDataJPA 整合QueryDSL 的findAll 来实现,
     *  第二个方法使用QueryDSL 本身的API 进行实现, 其中page 是Spring 自身的, 第一行 user.isNotNull.or(user.isNull)可以做到类似where
     *   1=1 的效果 以便于后面进行的拼接, 当然, 主键是并不能为空的, 所以此处也可以只写 user.isNotNull()也可以. 紧接着是一串的三目运算符
     *   表达式, 以第一行三目表达式为例, 表达意义自傲与: 如果用户名为空, 就返回定义好的predicate; 如果不为空, 就是用ExpressionUtils的and
     *   方法将username 作为条件进行组合ExpressionUtils的and方法返回值也是一个Predicate, 下面的几条三木运算符与第一条是类似的. 最后使用
     *   findAll 方法传入分页参数与条件参数predicate , 相比起上面头疼的自己拼接, 是不是 简洁了很多?
     *
     *　　最后一个方法展示的是如果有分组条件时进行的查询，　相信大家就字面意思连接也能知道大概的意思了．　前半部分代码是相同的，groupby 之后
     * 需要插入条件的是要用到的 having 的。 就与sql 的规范一样。 如果对于分组与having 还不了解， 可以查查百度
     *
     *
     *
     *   在应用开发中我们可能不会在代码中设置 @ManytoOne  @ManytoMany 这种类型的“强建模”， 而是在随从的Entity 中仅仅声明一个外键属性
     *   比如User 实体的下面代码， 只是添加了一个 departmentId
     *
     *
     *
     *
     */


    /**
     *
     *  根据部门id 查询用户的基本信息 + 用户所属部门信息， 并且使用UserDeptDTO 进行封装返回给前端。
     *
     *  我们在from 中多加了一个 departmentId 参数， 在where 中 多加了一个 user.department.eq(department.deptId)条件， 与sql 中的操作类似。
     * 为什么 在这里不适用 join ... on  呢， 原因是我们使用的QueryDSL-JPA, QueryDSL-jpa 支持是全面的， 当然也有QueryDSL-SQL ， 但是配置起来会
     * 比较麻烦。
     *
     *
     * @param deptId
     * @return
     */
    public List<UserDeptDTO> findByDepartmentIdDTO(Long deptId){
        QUser user = QUser.user;
        QDepartment department = QDepartment.department;

        // 直接返回
        return  jpaQueryFactory
                // 投影只取部分字段
                .select(
                        user.userName,
                        user.password,
                        user.name,
                        user.id,
                        department.deptName,
                        department.createDate
                ).from(user,department)
                // 联合查询
                .where(
                        user.departmentId.eq(department.deptId)
                        .and(department.deptId.eq(deptId))
                ).fetch()
                .stream()
                // 需要做类型转换， 所以使用 map 函数非常适合
                .map(tuple ->
                        UserDeptDTO.builder()
                .userName(tuple.get(user.userName))
                .name(tuple.get(user.name))
                .password(tuple.get(user.password))
                .id(tuple.get(user.id))
                .deptName(tuple.get(department.deptName))
                .deptBirth(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(tuple.get(department.createDate)))
                .build()
                ).collect(Collectors.toList());

    }


    /**
     * 结语： 使用SpringDataJPA 能够解决我们大多数的问题， 但是在处理复杂条件， 动态条件， 投影查询坑QueryDSL-JPA 更加直观，而且
     *  SpringDataJPA 对QueryDSL 有着非常好的支持, SpringDataJPA + QueryDSL 在我眼里看来是天生一对， 互相补漏，在使用一般查询，能
     *  够满足基础条件的查询我们使用SpringDataJPA 更加简介方便， 当遇到复杂，投影，动态查询时候我们可以考虑使用QueryDSL 做开发。 以上方案
     *  可以解决大多数持久层开发问题。当然，如果问题特别刁钻， 还是不能满足你的需求， 你也可以考虑直接操作HQL或者SQL
     *
     *
     *
     */


    public List<User> findAllUsers() {
        List<User> userList = userDao.findAll();
        return userList;
    }


}
